﻿using MakeWeekConcerts.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.Caching;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;

namespace MakeWeekConcerts
{
    public static class DataLoader
    {
        private static Concerts ConcertData { get; set; }

        private static IDictionary<string, List<ConcertData>> ConcertsByBand { get; set; }

        private static IDictionary<string, HashSet<string>> SimilarArtists { get; set; }

        public static string GetDataDirectory(string subPath)
        {
            var dataRoot = ConfigurationManager.AppSettings["DataRoot"];
            if (string.IsNullOrWhiteSpace(dataRoot)
                || !Directory.Exists(dataRoot))
                throw new ArgumentException("Invalid DataRoot setting: " + dataRoot);
            return Path.Combine(dataRoot, subPath);
        }

        public static List<ConcertData> ConcertsFor(string bandName)
        {
            List<ConcertData> result;
            if (ConcertsByBand.TryGetValue(bandName, out result))
                return result;
            return new List<ConcertData>();
        }

        public static Concerts GetConcerts()
        {
            return ConcertData;
        }

        public static List<string> GetSimilar(string artist)
        {
            HashSet<string> result;
            if (SimilarArtists.TryGetValue(artist, out result))
                return result.ToList();

            return new List<string>();
        }

        public static async Task<SimilarArtistsResponse> FindSimilarArtistsAsync(string artistName)
        {
            return await DataLoader_LastFM.LoadOrGetSimilarArtistsAsync(artistName);
        }

        public static async Task LoadConcertDataAsync(double minimumSimilarityThreshold = 0.3)
        {
            if (ConcertData == null)
            {
                ConcertData = await DataLoader_StrangerEvents.LoadConcertDataAsync(minimumSimilarityThreshold);
                ConcertsByBand = DataLoader_StrangerEvents.LoadConcertsByBand(ConcertData);
                SimilarArtists = await DataLoader_StrangerEvents.LoadSimilarArtistsAsync(ConcertData, DataLoader_LastFM.LoadOrGetSimilarArtistsAsync);
            }
        }
    }

    internal class DataLoader_StrangerEvents
    {
        public const string DefaultStrangerEventDataFile = @"strangerConcerts.json";

        private static string StrangerEventsDataFile()
        {
            return DataLoader.GetDataDirectory(DefaultStrangerEventDataFile);
        }

        public static Concerts LoadConcertsFromStrangerData()
        {
            var ser = JsonSerializer.Create();
            using (var reader = new JsonTextReader(new StreamReader(StrangerEventsDataFile())))
            {
                return ser.Deserialize<Concerts>(reader);
            }
        }

        public static IDictionary<string, List<ConcertData>> LoadConcertsByBand(Concerts concerts)
        {
            var concertsByBand = new Dictionary<string, List<ConcertData>>(StringComparer.OrdinalIgnoreCase);
            foreach (var concert in concerts.ConcertList)
            {
                foreach (var band in concert.Artists)
                {
                    List<ConcertData> bandShows = null;
                    if (!concertsByBand.TryGetValue(band, out bandShows))
                    {
                        bandShows = new List<ConcertData>();
                        concertsByBand[band] = bandShows;
                    }
                    bandShows.Add(concert);
                }
            }
            return concertsByBand;
        }

        public static async Task<Concerts> LoadConcertDataAsync(double minimumSimilarityThreshold = 0.3)
        {
            try
            {
                var concertData = LoadConcertsFromStrangerData();
                if (concertData.LastRefresh < DateTime.UtcNow.AddDays(-7))
                {
                    concertData = await new StrangerConcertParser().FetchAndParseConcertsAsync();
                    var serializer = JsonSerializer.Create();
                    using (var writer = new JsonTextWriter(new StreamWriter(StrangerEventsDataFile())))
                    {
                        serializer.Serialize(writer, concertData);
                    }
                }
                foreach (var show in concertData.ConcertList)
                {
                    show.Id = Guid.NewGuid().ToString();
                }
                return concertData;
            }
            catch (Exception e)
            {
                Trace.TraceError("Failed to deserialize concert data: {0}", e);
                return null;
            }
        }

        public static async Task<IDictionary<string, HashSet<string>>> LoadSimilarArtistsAsync(Concerts concertData, Func<string, Task<SimilarArtistsResponse>> GetSimilarAsync, double minimumSimilarityThreshold = 0.3)
        {
            var similarArtists = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
            foreach (var band in concertData.ConcertList.SelectMany(c => c.Artists).Distinct())
            {
                var similar = await GetSimilarAsync(band);
                var curSimilar = similar.SimilarArtists.Artists.Where(x => x.Match > minimumSimilarityThreshold).Select(x => x.Name).Concat(new string[] { band }).ToList();
                foreach (var artist in curSimilar)
                {
                    HashSet<string> similarList;
                    if (!similarArtists.TryGetValue(artist, out similarList))
                    {
                        similarList = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                        similarArtists[band] = similarList;
                    }
                    curSimilar.ForEach(x => similarList.Add(x));
                }
            }

            return similarArtists;
        }
    }

    internal class DataLoader_LastFM
    {
        private static MemoryCache SimilarCache = new MemoryCache("SimilarArtists");

        private const string LastFmSimilarArtistsUrl = "http://ws.audioscrobbler.com/2.0/?method=artist.getsimilar&artist={1}&api_key={0}&format=json";
        private const string DefaultSimilarArtistFileFmt = @"similarTo.{0}.json";

        public static string GetArtistFilename(string artistName)
        {
            var outputFileFmt = DataLoader.GetDataDirectory(DefaultSimilarArtistFileFmt);
            var cleanedName = artistName;
            foreach (char lDisallowed in System.IO.Path.GetInvalidFileNameChars())
            {
                cleanedName = cleanedName.Replace(lDisallowed.ToString(), "");
            }
            foreach (char lDisallowed in System.IO.Path.GetInvalidPathChars())
            {
                cleanedName = cleanedName.Replace(lDisallowed.ToString(), "");
            }
            cleanedName = new Regex(@"\s+").Replace(cleanedName, "_");
            outputFileFmt = outputFileFmt ?? DefaultSimilarArtistFileFmt;
            var artistOutputFile = string.Format(outputFileFmt, cleanedName);
            return artistOutputFile;
        }

        public static async Task<SimilarArtistsResponse> LoadOrGetSimilarArtistsAsync(string artistName)
        {
            if (SimilarCache.Contains(artistName))
            {
                return (SimilarArtistsResponse)SimilarCache.Get(artistName);
            }

            var outputFile = GetArtistFilename(artistName);
            if (!File.Exists(outputFile))
            {
                var similar = await GetSimilarArtistsAsync(artistName);
                var ser = JsonSerializer.Create();
                using (var writer = new JsonTextWriter(new StreamWriter(outputFile)))
                {
                    ser.Serialize(writer, similar);
                }
                return similar;
            }

            return await LoadSimilarArtistsAsync(artistName);
        }

        public static async Task<SimilarArtistsResponse> LoadSimilarArtistsAsync(string artistName)
        {
            if (SimilarCache.Contains(artistName))
            {
                return (SimilarArtistsResponse)SimilarCache.Get(artistName);
            }

            var outputFile = GetArtistFilename(artistName);
            if (!File.Exists(outputFile))
                return new SimilarArtistsResponse() { SimilarArtists = new SimilarArtists() };

            var ser = JsonSerializer.Create();
            SimilarArtistsResponse similar;
            using (var reader = new JsonTextReader(new StreamReader(outputFile)))
            {
                similar = ser.Deserialize<SimilarArtistsResponse>(reader);
            }

            if (similar.LastRefresh < DateTime.UtcNow.AddDays(-30))
            {
                Trace.TraceInformation("Similar artists for {0} are too old, refreshing.", artistName);
                similar = await GetSimilarArtistsAsync(artistName);
                using (var writer = new JsonTextWriter(new StreamWriter(outputFile)))
                {
                    ser.Serialize(writer, similar);
                }
            }

            SimilarCache.Add(artistName, similar, new CacheItemPolicy() { SlidingExpiration = TimeSpan.FromDays(1) });
            return similar;
        }

        public static async Task<SimilarArtistsResponse> GetSimilarArtistsAsync(string artistName)
        {
            if (SimilarCache.Contains(artistName))
            {
                return (SimilarArtistsResponse)SimilarCache.Get(artistName);
            }

            var client = new HttpClient();
            var encodedName = HttpUtility.UrlEncode(artistName.Trim());
            var uri = string.Format(LastFmSimilarArtistsUrl, ConfigurationManager.AppSettings["LastFM:APIKey"], encodedName);
            Trace.TraceInformation("Fetching similar for {0} from {1}", artistName, uri);
            var response = await client.GetAsync(uri);
            if (response.StatusCode != System.Net.HttpStatusCode.OK)
                return null;

            var ser = JsonSerializer.Create();
            using (var reader = new JsonTextReader(new StreamReader(await response.Content.ReadAsStreamAsync())))
            {
                SimilarArtistsResponse similarArtists = null;
                try
                {
                    similarArtists = ser.Deserialize<SimilarArtistsResponse>(reader);
                }
                catch (JsonSerializationException e)
                {
                    Trace.TraceError("Failed to find any similar artists for {0}: {1}", artistName, e.Message);
                    similarArtists = new SimilarArtistsResponse() { SimilarArtists = new SimilarArtists() };
                }
                if (similarArtists.SimilarArtists == null)
                {
                    similarArtists.SimilarArtists = new SimilarArtists();
                    Trace.TraceWarning("Failed to load SimilarArtists for {0}", artistName);
                }
                if (similarArtists.SimilarArtists.Artists == null)
                {
                    similarArtists.SimilarArtists.Artists = new List<Artist>();
                    Trace.TraceWarning("Failed to find any similar artists for {0}", artistName);
                }
                similarArtists.LastRefresh = DateTime.UtcNow;
                SimilarCache.Add(artistName, similarArtists, new CacheItemPolicy() { SlidingExpiration = TimeSpan.FromDays(1) });
                return similarArtists;
            }
        }

    }
}